Angular 團隊在 Angular 版本 19 中發布了 resource
和 rxResource
函數,以方便資料檢索。resource
的 loader
函數產生一個 Promise, rxResource
的 loader
函數產生一個Observable。 resource
和 rxResource
函數最終都會傳回一個 ResourceRef
。 如果應用程式使用 HttpClient 傳回 Observable,工程師可以使用 rxjs-interop
套件中的rxResource
函數重構程式碼。
我有一個舊的 Angular 16 項目,它使用 HttpClient 向伺服器發出 HTTP 請求以檢索 Pokemon。 連結:https://github.com/railsstudent/ng-pokemon-signal/tree/main/projects/pokemon-signal-demo-10/ 。 我將在這篇文章中用 19.0.0-next.11 版本重寫項目,並應用這兩個函數來檢索資料。
// pokemon.adapter.ts
import { Ability, DisplayPokemon, Pokemon, Statistics } from './interfaces/pokemon.interface';
export const pokemonAdapter = (pokemon: Pokemon): DisplayPokemon => {
const { id, name, height, weight, sprites, abilities: a, stats: statistics } = pokemon;
const abilities: Ability[] = a.map(({ ability, is_hidden }) => ({
name: ability.name, isHidden: is_hidden }));
const stats: Statistics[] = statistics.map(({ stat, effort, base_stat }) => ({ name: stat.name, effort, baseStat: base_stat }));
return {
id,
name,
… other properties
}
}
pokemonAdapter
函數將 HTTP response 的 Pokemon 屬性變更為元件期望的形狀。
// pokemon.service.ts
import { Injectable, resource, signal } from '@angular/core';
import { DisplayPokemon, Pokemon } from '../interfaces/pokemon.interface';
import { pokemonAdapter } from '../pokemon.adapter';
@Injectable({
providedIn: 'root'
})
export class PokemonService {
private readonly pokemonId = signal(1);
readonly pokemonResource = resource<DisplayPokemon, number>({
request: () => this.pokemonId(),
loader: async ({ request: id, abortSignal }) => {
try {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`, { signal: abortSignal });
const result = await response.json() as Pokemon
return pokemonAdapter(result);
} catch (e) {
console.error(e);
throw e;
}
}
});
updatePokemonId(value: number) {
this.pokemonId.set(value);
}
}
PokemonService 服務使用 resource
函數透過 ID 檢索 Pokemon。 resource
選項有四個屬性:request
、loader
、equal
和 injector
,對於使用 Angular Signal 的開發人員來說,這些屬性應該要很熟悉。 這個範例我只使用了 request
和 loader
; request
選項是一個追蹤 pokemonId
訊號的函數。 當訊號更新時,loader
函數執行 fetch,透過 ID 檢索 Pokemon,並解析 Promise 以獲得結果。 然後,pokemonAdapter
函數在將最終結果傳回給元件之前轉換 HTTP 回應。resource
函數在 untracked
內部運行 loader
函數;因此, loader
函數中的任何訊號變化都不會導致任何計算和重新運行。
loader
函數的參數是 ResourceLoaderParams
類型。它具有以下屬性:
如果我們想取消之前正在運行的請求,我們將把 abortSignal 傳遞給 fetch 呼叫。如果我們不使用 abortSignal,resource
函數將丟棄被後續請求取消的請求的結果。resource
的行為類似 RxJS 的 switchMap
運算子。
// pokemon.component.html
<h2>{{ title }}</h2>
<div>
@let resource = pokemon.value();
@let hasValue = pokemon.hasValue();
@let isLoading = pokemon.isLoading();
<p>Has Value: {{ hasValue }}</p>
<p>Status: {{ pokemon.status() }}. Status Enum: 0 - Idle, 1 - Error, 2 - Loading, 4 is Resolved.</p>
<p>Is loading: {{ isLoading }}</p>
<p>Error: {{ pokemon.error() }}</p>
@if (isLoading) {
<p>Loading the pokemon....</p>
} @else if (resource) {
<div class="container">
<img [src]="resource.frontShiny" />
<img [src]="resource.backShiny" />
</div>
<app-pokemon-personal [pokemon]="resource"></app-pokemon-personal>
<app-pokemon-tab [pokemon]="resource"></app-pokemon-tab>
<app-pokemon-controls [(search)]="pokemonId"></app-pokemon-controls>
}
</div>
這是 PokemonComponet 和 RxPokemonCoponent 元件的共用範本。resource
函數的傳回類型是 ResourceRef
,它具有以下訊號屬性:
ResourceRef
擴充了 WritableResource
;它公開 set()
和 update()
來更新資源。當它發生時,狀態變成 Local。
// search-input.operator.ts
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, filter, map, Observable } from "rxjs";
import { POKEMON_MAX, POKEMON_MIN } from '../constants/pokemon.constant';
export const searchInput = (minPokemonId = POKEMON_MIN, maxPokemonId = POKEMON_MAX) => {
return (source: Observable<number>) => source.pipe(
debounceTime(300),
filter((value) => value >= minPokemonId && value <= maxPokemonId),
map((value) => Math.floor(value)),
distinctUntilChanged(),
takeUntilDestroyed()
);
}
// pokemon.component.ts
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [PokemonControlsComponent, PokemonPersonalComponent, PokemonTabComponent],
templateUrl: './pokemon.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class PokemonComponent {
private readonly pokemonService = inject(PokemonService);
pokemonId = signal(1);
pokemon = this.pokemonService.pokemonResource;
constructor() {
toObservable(this.pokemonId).pipe(searchInput())
.subscribe((value) => this.pokemonService.updatePokemonId(value));
}
}
pokemonId
訊號以 two-way binding 方式綁定到 PokemonControlsComponent
元件的 search
model input。 pokemonId
更新時,toObservable
會向自訂 RxJS 運算子發送該值,以便在服務中設定 pokemonId
訊號之前進行 300 毫秒的 debounce。 它導致 loader
函數呼叫後端來檢索新資料並更新視圖 (view)。
// rx-pokemon.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { catchError, delay, map, of } from 'rxjs';
import { DisplayPokemon, Pokemon } from '../interfaces/pokemon.interface';
import { pokemonAdapter } from '../pokemon.adapter';
@Injectable({
providedIn: 'root'
})
export class RxPokemonService {
private readonly httpClient = inject(HttpClient);
private readonly pokemonId = signal(1);
readonly pokemonRxResource = rxResource<DisplayPokemon | undefined, number>({
request: () => this.pokemonId(),
loader: ({ request: id }) => {
return this.httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
.pipe(
delay(500),
map((pokemon) => pokemonAdapter(pokemon)),
catchError((e) => {
console.error(e);
return of(undefined);
})
);
}
});
updatePokemonId(input: number) {
this.pokemonId.set(input);
}
}
此服務與 PokemonService
服務類似,不同之處在於 pokemonRxResource
成員呼叫 rxResource
函數來取得 ResourceRef
。同樣, request
選項是追蹤 pokemonId
訊號的函數。 loader
函數使用 HttpClient 請求 HTTP GET 以透過 ID 檢索 Pokemon。
rxResource
函數的行為與 firstValueFrom
相同;僅考慮 Observable stream 的第一次 emission。 loader
函數將值傳送給 take(1)
,該值接受第一次 emission 或透過 takeUntil
取消一個 cancel Subject。
// rx-pokemon.component.ts
@Component({
selector: 'app-rx-pokemon',
standalone: true,
imports: [PokemonControlsComponent, PokemonPersonalComponent, PokemonTabComponent],
templateUrl: './pokemon.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class RxPokemonComponent {
private readonly pokemonService = inject(RxPokemonService);
pokemon = this.pokemonService.pokemonRxResource;
pokemonId = signal(1)
constructor() {
toObservable(this.pokemonId).pipe(searchInput())
.subscribe((value) => this.pokemonService.updatePokemonId(value));
}
}
pokemonId
訊號以 two-way binding 方式綁定到 PokemonControlsComponent
的 search
model input。 pokemonId
更新時, toObservable
會向自訂 RxJS 運算子發送該值,以便在服務中設定 pokemonId
訊號之前進行 300 毫秒的 debounce。 它導致 loader
函數呼叫後端來檢索新資料並更新視圖 (view)。
resource
函數監聽 request
並發出 HTTP 請求以透過 ID 檢索 Pokemon。resource
函數會丟棄取消的請求的結果。resource
的 loader
函數傳回一個 Promise,rxResource
的 loader
函數傳回一個 Observable。resource
和 rxResource
會建立一個由許多 Signal 屬性組成的 ResourceRef
。rxjs-interop
套件中的 rxResource
函數。rxResource
函數在底層使用 AbortSignal;因此,它不會將 abortSignal 傳遞到 HttpClient。rxResource
的行為類似 RxJS 的 firstValueFrom
,傳回 Observable stream 的第一個 emission。 loader
函數完成後,rxResource
函數可以接受來自元件的新請求。鐵人賽的第 39 天到此結束